Skip to content

场景

储存所有几何体的对象

js
// 背景色
scene.background = new THREE.Color(0xcccccc);

// 环境贴图(将作用所有物体,常用于让模型被打亮)
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";
const rgbeLoader = new RGBELoader();
rgbeLoader.load("/public/Alex_Hart-Nature_Lab_Bones_2k.hdr", (envMap) => {
  envMap.mapping = THREE.EquirectangularReflectionMapping; // 球面全景图映射
  scene.environment = envMap;  // 设置场景环境贴图
});

快速上手

注:外部的模型,一律放在public静态资源文件夹里,一般路径是/public/模型文件

基本模型

基本图形:通过内置构造器创建

js
const geometry = new THREE.BoxGeometry(1, 1);  // 几何体
const material = new THREE.MeshBasicMaterial({ color: "#6cf" });  // 材质
const cube = new THREE.Mesh(geometry, material);   // 几何体+材质

特殊模型:雾

以摄像头为起点,到一定距离之后开始起雾

js
scene.background = new THREE.Color(0xcccccc);  // 建议背景颜色和雾相近
scene.fog = new THREE.Fog(0xcccccc, 0.1, 50);  // 线性雾 (颜色, 起效距离, 纯雾距离)
scene.fog = new THREE.FogExp2(0xcccccc, 0.05);  // 指数雾 (颜色,指数)

外部模型:glb

js
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
const gltfLoader = new GLTFLoader();
gltfLoader.load("/public/Duck.glb", (root) => scene.add(root.scene));

外部模型:glb(压缩)

额外步骤:将依赖包复制到静态目录

js
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
const dracoLoader = new DRACOLoader();
const gltfLoader = new GLTFLoader();
 // 解析文件,在three依赖包里,路径:node_modules/three/examples/jsm/libs/draco
dracoLoader.setDecoderPath("./draco/");   // 将整个文件复制到静态目录
gltfLoader.setDRACOLoader(dracoLoader);
gltfLoader.load("/public/city.glb", (root) => scene.add(root.scene));

属性

模型

常用属性

js
visible: boolean;  // 是否隐藏

变形

js
rotation: Vector3; // 旋转,180度是 Math.PI * 2
position: Euler; // 位移,相对于父元素的坐标(scene子项的相对坐标就是世界坐标)
scale: Vector3;  // 缩放

// type
Vector3(x:float,y:float,z:float);  // 三维向量
Euler(x:float,y:float,z:float,order:string);  // 欧拉角,order旋转顺序,默认'XYZ'

// Fn
属性.set(number,number,number)  // 修改
属性.x = number  // 修改单轴

常用几何构造器

js
// 共用一个材质
const material = new THREE.MeshBasicMaterial({
  color: "#6cf",
  wireframe: true,
});

// 平面
{
  // x长度: number,y长度: number, x细分面: number, y细分面: number
  const geometry = new THREE.PlaneGeometry(1, 1);
  const plane = new THREE.Mesh(geometry, material);
  scene.add(plane);
}

// 正方体
{
  // x轴长度: number, y轴长度: number, z轴长度: number, ?x轴分段数: number, ?y轴分段数: number, ?z轴分段数: number
  const geometry = new THREE.BoxGeometry(1, 1, 1, 2);
  const cube = new THREE.Mesh(geometry, material);
  scene.add(cube);
}

// 圆形
{
  // 半径: number, 分段数: number, 起始角度(2π)?: number, 展开角度(2π)?: number
  const geometry = new THREE.CircleGeometry(5, 32, 0, 3.14);
  const circle = new THREE.Mesh(geometry, material);
  scene.add(circle);
}

// 圆锥
{
  // 半径: number,y轴高: number,圆面细分: number,椎体截面细分?: number, 是否隐藏圆面?: boolean, 起始角度(2π)?: number, 展开角度(2π)?: number
  const geometry = new THREE.ConeGeometry(5, 5, 32, 10, true);
  const cone = new THREE.Mesh(geometry, material);
  scene.add(cone);
}

// 圆柱
{
  // 顶面半径: number,底面半径: number,y轴高: number,圆面细分: number,椎体截面细分?: number, 是否隐藏圆面?: boolean 起始角度(2π)?: number, 展开角度(2π)?: number
  const geometry = new THREE.CylinderGeometry(1, 3, 5, 10);
  const cylinder = new THREE.Mesh(geometry, material);
  scene.add(cylinder);
}

// 球体
{
  // 半径: number,圆面细分: number, 截面细分: number, 起始角度(2π)?: number, 展开角度(2π)?: number,垂直面起始角度(π)?: number, 展开角度(π)?: number
  const geometry = new THREE.SphereGeometry(3, 30, 1);
  const sphere = new THREE.Mesh(geometry, material);
  scene.add(sphere);
}

// 球体(8面体)
{
  // 半径: number,分段数?: number
  const geometry = new THREE.TetrahedronGeometry(2, 1);
  const tetrahedron = new THREE.Mesh(geometry, material);
  scene.add(tetrahedron);
}

// 球体(8面体)
{
  // 半径: number,分段数?: number
  const geometry = new THREE.OctahedronGeometry(2);
  const octahedron = new THREE.Mesh(geometry, material);
  scene.add(octahedron);
}

// 球体(12面体)
{
  // 半径: number,分段数?: number
  const geometry = new THREE.DodecahedronGeometry(2);
  const dodecahedron = new THREE.Mesh(geometry, material);
  scene.add(dodecahedron);
}

// 球体(20面体)
{
  // 半径: number,分段数?: number
  const geometry = new THREE.IcosahedronGeometry(2);
  const icosahedron = new THREE.Mesh(geometry, material);
  scene.add(icosahedron);
}

// 环形
{
  // 内径: number, 外径: number, 圆面分段: number, 树根分轮: number, 起始角度(2π)?: number, 展开角度(2π)?: number
  const geometry = new THREE.RingGeometry(1, 3, 20, 5);
  const ring = new THREE.Mesh(geometry, material);
  scene.add(ring);
}

// 圆环(甜甜圈)
{
  // 圆半径: number, 环半径: number, 圆分段?: number, 环分段?: number, 展开角度(2π)?: number
  const geometry = new THREE.TorusGeometry(3, 1, 30, 30);
  const torus = new THREE.Mesh(geometry, material);
  scene.add(torus);
}

材质

js
const material = new THREE.MeshBasicMaterial();

material.color = "#fff";  // 基本材质_颜色
material.transparent = true;  // 允许透明度
material.side = THREE.DoubleSide, // 两面可见
material.wireframe = true, // 线框模式

// 加载外部纹理
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load("/public/texture/材质");
material.map = texture; // 材质
texture.colorSpace = THREE.SRGBColorSpace; // 色彩空间_sRGB

// AO贴图: 正片叠底(暗部闭塞叠加)
const aoMap = textureLoader.load("/public/texture/AO贴图");
material.aoMap = aoMap;
material.aoMapIntensity = 0.5; // ao贴图强度

// alpha贴图: 蒙版(黑透明,白不透明)
const alphaMap = textureLoader.load("/public/texture/alpha贴图");
material.alphaMap = alphaMap;

// 光照贴图: 颜色叠加
const lightMap = textureLoader.load("/public/texture/光照贴图");
material.lightMap = lightMap;


// 环境贴图: 镜面反射
const rgbeLoader = new RGBELoader();
rgbeLoader.load(
  "/public/texture/Alex_Hart-Nature_Lab_Bones_2k.hdr",
  (envMap) => {
    envMap.mapping = THREE.EquirectangularReflectionMapping; // 球面全景图映射
    // scene.background = envMap;  // 可设置成vr场景
    // scene.environment = envMap;
    material.envMap = envMap;
    material.reflectivity = 0.1;  // 反射强度
  }
);

// 高光贴图:反射度蒙版
const specularMap = textureLoader.load("/public/texture/高光贴图");
material.specularMap = specularMap;

材质属性

js
wireframe: boolean;  // 显示为线框

组件

OrbitControls轨道控制器

new OrbitControls();

通过监听DOM的鼠标事件,控制摄像机移动 OrbitControls – three.js docs (threejs.org)

参数: - 摄像机 - 被监听的DOM

js
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
const controls = new OrbitControls(camera, renderer.domElement);

controls.minDistance = 5;  // 最小缩放
controls.maxDistance = 100;  // 最大缩放
controls.autoRotate = true;  // 自动旋转
controls.enableDamping = true;  // 设置阻尼(惯性)
controls.dampingFactor = 0.001;  //  阻尼系数(越小越难推动)
controls.target.set(0, 0, 0);  // 视线目标
controls.enablePan = false;  // 禁用平移,shift拖拽平移
controls.enableRotate = false;  // 禁用旋转,拖拽
controls.enableZoom = false;  // 禁用缩放,滚轮

controls.update();  // *更新

Raycaster射线

重要,用户和模型交互的纽带 从鼠标位置发出射线,获取被击中的模型

js
// 设靶子环节
const geometry = new THREE.BoxGeometry(10, 10);
const material = new THREE.MeshBasicMaterial({ color: "#fff" });
const cube1 = new THREE.Mesh(geometry, material);
const cube2 = new THREE.Mesh(geometry, material);
const cube3 = new THREE.Mesh(geometry, material);
scene.add(cube1);
scene.add(cube2);
scene.add(cube3);
cube1.position.x = -30;
cube3.position.x = 20;


// 正片
const raycaster = new THREE.Raycaster(); // 射线
const mouse = new THREE.Vector2(); // 存储鼠标向量

// 通过事件触发
window.addEventListener("click", (ev) => {
  // 计算,鼠标位于屏幕的位置 -> 平面坐标系的位置(-1到1)
  mouse.x = (ev.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(ev.clientY / window.innerHeight) * 2 + 1;

  raycaster.setFromCamera(mouse, camera); // 创建射线,从相机到鼠标向量
  
  // 可从场景中检测内容
  const intersects = raycaster.intersectObjects(scene.children); 
  
  // 可从若干个对象中检测内容
  const intersects = raycaster.intersectObjects([cube1, cube2, cube3]);

  intersects[n].distance; // number 接触点距离,数组根据该属性从近到远排列
  intersects[n].face; // 接触面信息
  intersects[n].normal; // 接触点法相向量
  intersects[n].object; // *所选物体
  intersects[n].point; // 接触点坐标
  intersects[n].uv; // 接触uv
});

AxesHelper世界坐标辅助器

new THREE.AxesHelper() 参数:坐标线段长度 注:红色X轴,绿色Y轴,蓝色Z轴

js
import * as THREE from "three";
const AxesHelper = new THREE.AxesHelper(5);

scene.add(AxesHelper);

GUI属性管理控件

new GUI();

快速创建表单元素,用于控制变量 第一个参数是监听区域,第二个是监听值

实例.add(); 参数: 1. 监听区域 2. 监听值,根据值的类型改变表单类型 3. 简写内容,每一种类型都不同,不推荐用

js
import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js";
const gui = new GUI();
gui.add(...).name(...);

// gui属性
.add(...);  // 添加表单项
.name(string);  // 表单项label
.onChange((val) => console.log(val)); // 监听变化后的值
.onFinishChange((val) => console.log(val)); // 监听鼠标松开后的值

.min(number);  // 最小值,监听数字,如果没有最大值,则显示为数字输入框
.max(number);  // 最大值,监听数字,如果有最小值,则显示为滑块
.step(number);  // 最小变化单位,滑块


// 文件夹
let box = gui.addFolder("文件夹");
box.add(cube.position, "x").min(-10).max(10).step(1);
box.add(cube.position, "y").min(-10).max(10).step(1);
box.add(cube.position, "z").min(-10).max(10).step(1);


// 按钮:监听内容为【函数】
let eventObj = {  // 创建一个方法map
  Fullscreen: () => document.body.requestFullscreen(),  // 全屏
  exitFullscreen: () => document.exitFullscreen()  // 退出全屏
};
gui.add(eventObj, "Fullscreen").name("全屏"); 
gui.add(eventObj, "exitFullscreen").name("退出全屏");;

// 勾选框:监听内容为【布尔值】
const material = new THREE.MeshBasicMaterial();
gui.add(material, "wireframe");  // 监听材质的wirfram属性

// 滑块|输入框:监听内容为【数字】
gui.add(cube.position, "x").min(-10).max(10).step(1);
gui.add(cube.position, "x", -5, 5); // 简写


// 应用: 通过按钮改变颜色
let colorParms = {
  cubeColor: "#00ff00",
};

gui
  .add(colorParms, "cubeColor")  // 监听刚创建的对象
  .onChange((val) => {  // 改变内容触发事件
    cube.material.color.set(val);   // 改变材质颜色
  });

Tween补间动画

额外步骤:

js
import * as TWEEN from "three/examples/jsm/libs/tween.module";

// 施法材料_一个方块
const geometry = new THREE.BoxGeometry(10, 10);
const material = new THREE.MeshBasicMaterial({ color: "#fff" });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

// 动画fn
const tween = new TWEEN.Tween(cube.position); // 获取动画源
tween.to({ x: 10 }, 1000); // 设置动画值
tween.repeat(Infinity); // 循环
tween.yoyo(true); // 往复,会引起跳帧
tween.delay(50); // 延迟(每次循环),可解决跳帧
tween.easing(TWEEN.Easing.Quadratic.InOut); // 贝塞尔曲线https://tweenjs.github.io/tween.js/examples/03_graphs.html

// 事件
tween.onUpdate((val) => console.log(val)); // 每一帧
tween.onStart((val) => console.log(val)); // 每次(循环)开始
tween.onComplete((val) => console.log(val)); // 每次(循环)完成
tween.onStop((val) => console.log(val)); // 停止(执行.stop)时

// 嵌套,形成往复且不会跳帧
const tween2 = new TWEEN.Tween(cube.position); // 创建动画2
tween2.to({ x: 0 }, 1000);
tween.chain(tween2);
tween2.chain(tween);

// *重要
tween.start(); // 开始动画

// render循环内添加
function animate() {
  requestAnimationFrame(animate);
  controls.update();
  renderer.render(scene, camera);
  TWEEN.update();  // 下一帧的补间动画
}
animate();

加载器(未装填)

js
// 引入
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";  // hdr全景环境贴图

// 内置
const textureLoader = new THREE.TextureLoader();

原理

几何体

几何体由三角形组成,三角形由三个点位组成,每个点位有三个向量(Vector3)

底层属性

js
position: Float32Array;  // 用于描述几何体的顶点
index: Uint16Array;  // 用于描述顶点的顺序,用于合并重复的公用定点
groups: {start: number, count: number, materialIndex: number}[];  // 给材质分组

基本原理

所有几何体都是由三角形拼接而成 2个三角形 => 1个正方形 12个三角形 => 6个面 => 1个正方体

使用顶点,描述正方形
js
// 三角形
{
  const geometry = new THREE.BufferGeometry();  // 空几何体
  const vertices = new Float32Array([   // 点位顺序:逆时针为正面(反面默认不可见)
    -1.0, -1.0, 0.0,   // 点位1
    1.0, -1.0, 0.0,   // 点位2
    1.0, 1.0, 0.0,   // 点位3
  ]);

  // 给空几何体,添加描述形状的属性,以三个坐标为一个点
  geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
  const material = new THREE.MeshBasicMaterial({
    color: "#6cf",
    side: THREE.DoubleSide, // 两面可见
    wireframe: true, // 线框模式
  });
  const plane = new THREE.Mesh(geometry, material);
  scene.add(plane);
}

// 正方形(三角形拼接)
{
  const geometry = new THREE.BufferGeometry();
  const vertices = new Float32Array([
    -1.0, -1.0, 0.0,  // 三角形1
	1.0, -1.0, 0.0,
	1.0, 1.0, 0.0,
	0.5, 0.5, 0,  // 三角形2
	-0.5, 0.5, 0,
    -0.5, -0.5, 0,
  ]);
  geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
  const material = new THREE.MeshBasicMaterial({color: "#6cf"});
  const plane = new THREE.Mesh(geometry, material);
  scene.add(plane);
}

// 正方形(公用顶点拼接)
{
  const geometry = new THREE.BufferGeometry();
  const vertices = new Float32Array([
    -1.0, -1.0, 0.0,  // 点位1
    1.0, -1.0, 0.0,  // 点位2
    1.0, 1.0, 0.0,  // 点位3
    -1.0, 1.0, 0.0,  // 点位4
  ]);
  geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));

  // 配置公共顶点,0,1,2组成一个三角形  2,3,0组成一个三角形
  const indices = new Uint16Array([0, 1, 2, 2, 3, 0]);
  geometry.setIndex(new THREE.BufferAttribute(indices, 1));  // 几何体添加索引属性
  const material = new THREE.MeshBasicMaterial({color: "#6cf"});
  const plane = new THREE.Mesh(geometry, material);
  scene.add(plane);
}
一个几何体里使用多个材质

几何体的属性groups可用于分组

注:材质为分组时,材质少于几何体内的组,则没被定义的组透明

js
// 顶点分组,使用两种材质
{
  const geometry = new THREE.BufferGeometry();
  const vertices = new Float32Array([-1.0, -1.0, 0.0, 1.0, -1.0, 0.0, 1.0, 1.0, 0.0, -1.0, 1.0, 0.0]);
  geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
  const indices = new Uint16Array([0, 1, 2, 2, 3, 0]);

  // 设置顶点组
  geometry.setIndex(new THREE.BufferAttribute(indices, 1));
  geometry.addGroup(0, 3, 0); // 索引0开始,添加3个顶点,使用材质0
  geometry.addGroup(3, 3, 1); // 索引3开始,添加3个顶点,使用材质1

  // 配置两种材质
  const material1 = new THREE.MeshBasicMaterial({ color: "red" });
  const material2 = new THREE.MeshBasicMaterial({ color: "#6cf" });
  const plane = new THREE.Mesh(geometry, [material1, material2]); // 材质是数组
  scene.add(plane);
}


// 内置几何体的默认分组
{
  const geometry = new THREE.BoxGeometry(1, 1, 1);
  const material1 = new THREE.MeshBasicMaterial({ color: "red" });
  const material2 = new THREE.MeshBasicMaterial({ color: "#6cf" });

  // 正方体默认6个组,表示6个面
  const cube = new THREE.Mesh(geometry, [
    material1,
    material2,
    material1,
    material1,
    material1,
    material1,
  ]);
  scene.add(cube);
}

材质

uv贴图

模型使用uv贴图时,会把贴图列成一个坐标系,→U ↑V 模型的每个顶点都会对应一个坐标,匹配到贴图的位置。

js
// 正方形_顶点拼接
{
  const geometry = new THREE.BufferGeometry();
  const textureLoader = new THREE.TextureLoader();
  const texture = textureLoader.load("/public/texture/uv_grid_opengl.jpg");
  const vertices = new Float32Array([
    -1.0, -1.0, 0.0, 1.0, -1.0, 0.0, 1.0, 1.0, 0.0, -1.0, 1.0, 0.0,
  ]);
  const indices = new Uint16Array([0, 1, 2, 2, 3, 0]);
  geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
  geometry.setIndex(new THREE.BufferAttribute(indices, 1));

  // 标记uv图的四个点位:左下,右下,右上,左上
  const uv = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]);
  // 设置uv属性,用于map材质。两个数字对应一个uv坐标
  geometry.setAttribute("uv", new THREE.BufferAttribute(uv, 2)); 

  const material = new THREE.MeshBasicMaterial({ map: texture });
  const plane = new THREE.Mesh(geometry, material);
  scene.add(plane);
  plane.position.x = -1;
}

  

// 正方体_内置几何体
{
  const geometry = new THREE.PlaneGeometry(2, 2);  // 内置几何体自带uv坐标
  const textureLoader = new THREE.TextureLoader();
  const texture = textureLoader.load("/public/texture/uv_grid_opengl.jpg");
  const material = new THREE.MeshBasicMaterial({ map: texture });
  const plane = new THREE.Mesh(geometry, material);
  scene.add(plane);
  plane.position.x = 1;
}

解决方案

画布自适应窗口

js
function resize() {
  renderer.setSize(window.innerWidth, window.innerHeight); // 更新渲染区宽高
  camera.aspect = window.innerWidth / window.innerHeight; // 更新摄像机长宽比
  camera.updateProjectionMatrix(); // 更新摄像机
}

resize();  // 初始化,让自适应窗口
window.addEventListener("resize", resize);  // 监听窗口变化事件

全屏按钮

js
renderer.domElement.requestFullscreen();  // 进入全屏,必须挂在在按钮
renderer.domElement.exitFullscreen();  // 退出全屏,必须挂在在按钮

// 游览器API,让元素全屏,不能直接触发,必须挂在在按钮上
Element.requestFullscreen();

加载src目录下文件

正常情况下,模型文件放在public静态目录里

js
// 默认加载public静态目录下文件
load('./...')
load('/public/...')  // 等同于

// 通过require引入,将基准从public转变为src
load('./asset/...')

纹理颜色空间

默认的颜色空间,可能会显得模型发白,这时考虑将色彩空间切换成sRGB

js
// 加载外部纹理
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load("/public/texture/xx.png");
material.map = texture; // 设置纹理
texture.colorSpace = THREE.SRGBColorSpace; // 色彩空间_sRGB ,更接近人眼直观感受
texture.colorSpace = THREE.LinearSRGBColorSpace; // 色彩空间_线性亮度 ,默认值

// 异步触发
gui
  .add(texture, "colorSpace", {
    sRGB: THREE.SRGBColorSpace,
    colorSpace: THREE.LinearSRGBColorSpace,
  })
  .onChange(() => {
    texture.needsUpdate = true; // 修改必须设置更新
  });

获取模型尺寸

js
const { min, max } = box3.setFromObject(target);  // x,y,z

摄像机位置

js
const cameraPosition = new THREE.Vector3(); // 摄像机位置